package edu.arizona.sista.learn.model.groups;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import edu.arizona.sista.learn.model.Interval;
import edu.arizona.sista.learn.model.ParamInterval;

/**
 * Models of various rules regarding grouping.
 * 
 * @author Anh Tran
 *
 */
public class GroupingRules {

	/**
	 * Min-duration
	 * Remove all groups with intervals less than minDuration.
	 * @param groups Groups to be filtered.
	 * @param minDuration Minimum duration theshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> minDuration(List<Group> groups, int minDuration,
			StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups with duration shorter than " +
				minDuration + " frames:\n\n");
		for (int i = groups.size() - 1; i >= 0; i--) {
			if (groups.get(i).interval().duration() < minDuration) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		return filtered;
	}
	
	/**
	 * Max-duration
	 * Remove all groups with intervals longer than maxDuration.
	 * @param groups Groups to be filtered.
	 * @param maxDuration Maximum duration theshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> maxDuration(List<Group> groups, int maxDuration,
			StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups with duration longer than " +
				maxDuration + " frames:\n\n");
		for (int i = groups.size() - 1; i >= 0; i--) {
			if (groups.get(i).interval().duration() > maxDuration) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		return filtered;
	}
	
	/**
	 * Min-participants
	 * Remove all groups with less than minParticipants members
	 * actively participating at any given time.
	 * @param groups Groups to be filtered.
	 * @param minParticipants Minimum participants theshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> minParticipants(List<Group> groups, int minParticipants,
			StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups with less than " +
				minParticipants + " participants:\n\n");
		for (int i = groups.size() - 1; i >= 0; i--) {
			if (groups.get(i).getMinParticipants() < minParticipants) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		return filtered;
	}
	
	/**
	 * Max-participants
	 * Remove all groups that have more than maxParticipants members
	 * actively participating at any given time.
	 * @param groups Groups to be filtered.
	 * @param maxParticipants Maximum participants theshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> maxParticipants(List<Group> groups, int maxParticipants,
			StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups with more than " +
				maxParticipants + " participants:\n\n");
		for (int i = groups.size() - 1; i >= 0; i--) {
			if (groups.get(i).getMaxParticipants() > maxParticipants) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		return filtered;
	}
	
	/**
	 * Continuity-of-participants
	 * In ascending order, merge any 2 groups that match the following criteria:
	 * 	- one group ends right before another begins (within some window)
	 * 	- both groups share the 'majority' of their participants with each other	
	 * @param groups Groups to be filtered.
	 * @param noiseWindow Noise window threshold for end-to-end intervals.
	 * @param minSharedRatio Majority ratio threshold for overlapping groups.
	 * @param out Output stream for debug.
	 */
	public static void mergeOverlappingParticipants(List<Group> groups, int noiseWindow,
			float minSharedRatio, StringBuilder out) {
		
		if (out == null) out = new StringBuilder();
		int current = 0;
		boolean next = (current < groups.size());
		
		while (next) {
			
			Group group1 = groups.get(current);
			int candidate = -1;

			// Search through subsequent ('future') groupings to find
			// a possible merge candidate
			for (int j = current + 1; j < groups.size(); j++) {

				// Check intervals alignment (with some noise toleration)
				if ((group1.interval().end() - noiseWindow > groups
						.get(j).interval().start())
						|| (group1.interval().end() + noiseWindow < groups
								.get(j).interval().start())) {
					continue;
				}

				// Find shared participants
				Set<Person> shared = new HashSet<Person>();
				Set<Person> g1parts = group1.getActiveParticipants(
						new Interval(group1.interval().end() - noiseWindow,
								group1.interval().end()));
				Set<Person> g2parts = groups.get(j).getActiveParticipants(
						new Interval(groups.get(j).interval().start(),
								groups.get(j).interval().start() + noiseWindow));
				for (Person part : g1parts) {
					if (g2parts.contains(part))
						shared.add(part);
				}

				// Check that the # of shared participants meet the ratio req.
				if (((float) shared.size() / g1parts.size() > minSharedRatio)
						&& ((float) shared.size() / g2parts.size() > minSharedRatio)) {
					candidate = j;
					break;
				}
			}

			// Merge groups if a valid candidate is found
			if (candidate != -1) {
				out.append("===\nMerging the following groups with over " +
						(minSharedRatio*100) + "% shared participants:\n\n"
						+ group1.toString() + "\n"
						+ groups.get(candidate).toString() + "\n");
				group1.mergeToEnd(groups.get(candidate), noiseWindow);
				groups.remove(candidate);
			} else {
				// Move to next group if no merging took place
				current = current + 1;
			}
			next = (current < groups.size());
		}
	}
	
	/**
	 * Min-Group-Attention-Gaze-Ratio
	 * Remove all groups where one or more participants did not spend at least
	 * minGazeRatio of their time in the group looking at another member of the
	 * group.  In other words, remove groups where participants did not spend
	 * enough time focusing on each other.
	 * @param groups Groups to be filtered.
	 * @param minGazeRatio Minimum looking attention threshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> minGroupAttentionGazeRatio(List<Group> groups,
			float minGazeRatio, StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups where one or more participants\n" +
				"did not focus their attention on the other group members for at least\n" +
				(minGazeRatio*100) + "% of the time:\n\n");
		
		boolean success; int lookDuration; Set<Thing> targets;
		for (int i = groups.size() - 1; i >= 0; i--) {
			success = true;
			for (ParamInterval<GroupFormation> grouping : groups.get(i).groupingVariations()) {
				// We'll need to enforce group's attention for each grouping variation
				for (Person p : grouping.obj().participants()) {
					// For each person in the group, add up all the timesteps that
					// they spent looking at another member of the group.
					lookDuration = 0;
					for (int t = grouping.interval().start(); t < grouping.interval().end(); t++) {
						targets = p.getLookAtTargets(t);
						for (Thing target : targets) {
							if (target instanceof Person && grouping.obj().contains((Person)target)) {
								lookDuration++;
								break;
							}
						}
					}					
					// Each person must spend at least minGazeRatio amount of time
					// looking at another group's member while they're in the group.
					if (((float)lookDuration/grouping.interval().duration()) < minGazeRatio) {
						success = false;
						break;
					}
				}
				if (!success) {
					// If any grouping variation fails, then the group fails.
					// No need to check other grouping variations.
					break;
				}
			}
			if (!success) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		
		return filtered;
	}
	
	/**
	 * Min-Shared-Attention-Gaze-Ratio
	 * Remove all groups where the participants did not spend at least
	 * minGazeRatio of their time focusing on the same object.
	 * @param groups Groups to be filtered.
	 * @param minGazeRatio Minimum looking attention threshold.
	 * @param out Output stream for debug.
	 * @return The removed groups.
	 */
	public static List<Group> minSharedAttentionGazeRatio(List<Group> groups,
			float minGazeRatio, StringBuilder out) {
		if (out == null) out = new StringBuilder();
		List<Group> filtered = new ArrayList<Group>();
		out.append("===\nRemoving the following groups where all participants\n" +
				"did not spend at least " + (minGazeRatio*100) + "% of the time\n" +
				"looking at the same thing:\n\n");

		boolean success; int lookDuration; Set<Thing> targets; Set<Thing> gTargets;
		for (int i = groups.size() - 1; i >= 0; i--) {
			success = true;
			for (ParamInterval<GroupFormation> grouping : groups.get(i).groupingVariations()) {
				// We'll need to enforce group's attention for each grouping variation
				lookDuration = 0;
				for (int t = grouping.interval().start(); t < grouping.interval().end(); t++) {
					// Find out what everyone in the group is looking at
					gTargets = null;
					for (Person p : grouping.obj().participants()) {
						targets = p.getLookAtTargets(t);
						if (gTargets == null) {
							gTargets = targets;
						} else {
							gTargets.retainAll(targets);
						}
					}
					
					// If everyone in the group was looking at a single thing, then
					// increment duration.
					if (gTargets.size() >= 1) {
						lookDuration += 1;
					}
				}
				// Everyone in the grouping variation must spend at least minGazeRatio
				// amount of time looking at the same thing.
				if (((float)lookDuration/grouping.interval().duration()) < minGazeRatio) {
					success = false;
					break;
				}
			}
			if (!success) {
				out.append(groups.get(i).toString() + "\n");
				filtered.add(groups.remove(i));
			}
		}
		
		return filtered;
	}

}
